﻿using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection.Emit;

namespace AE.Remoting {
    public static class FastReflection {
        private static CacheDictionary<RuntimeMethodHandle, Delegate> _Invokers = CacheDictionary.Create((RuntimeMethodHandle handle) => {
            MethodInfo method = (MethodInfo)MethodBase.GetMethodFromHandle(handle);
            if (method.ReturnType == typeof(void)) {
                return CreateSubMethodDelegate(method) as Delegate;
            } else {
                return CreateFunctionMethodDelegate(method) as Delegate;
            }
        });
        private static CacheDictionary<Type, FieldInfo[]> _Fields = CacheDictionary.Create((Type type) => type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance));
        private static CacheDictionary<Type, FieldInfo[]> _SerializableFields = CacheDictionary.Create((Type type) => type.GetCachedFields().Where(x => !x.IsNotSerialized).ToArray());
        private static CacheDictionary<Type, PropertyInfo[]> _Properties = CacheDictionary.Create((Type type) => type.GetProperties());
        private static CacheDictionary<Type, bool> _MarkedSerializable = CacheDictionary.Create((Type x) => x.IsSerializable || x.GetCustomAttributes(typeof(SerializableAttribute), true).Any());
        private static CacheDictionary<FieldInfo, Action<object, object>> _FieldSetters = CacheDictionary.Create((FieldInfo field) => CreateSetFieldDelegate(field));
        private static CacheDictionary<PropertyInfo, Action<object, object[]>> _PropSetters = CacheDictionary.Create((PropertyInfo prop) => CreateSetPropertyDelegate(prop));
        private static CacheDictionary<PropertyInfo, Func<object, object>> _PropGetters = CacheDictionary.Create((PropertyInfo prop) => CreateGetPropertyDelegate(prop));
        private static CacheDictionary<FieldInfo, Func<object, object>> _FieldGetters = CacheDictionary.Create((FieldInfo field) => CreateGetFieldDelegate(field));
        private static CacheDictionary<Type, Constructor[]> _Constructors = CacheDictionary.Create((Type type) => type.GetConstructors().Select(x => new Constructor(x)).ToArray());
        private static CacheDictionary<ConstructorInfo, Func<object[], object>> _ConstructorMethods = CacheDictionary.Create((ConstructorInfo method) => CreateConstructor(method));

        public class Constructor {
            public int NumArgs { get; set; }
            public Type[] ArgTypes { get; set; }
            public Func<object[], object> Method { get; set; }
            public Constructor(ConstructorInfo con) {
                var @params = con.GetParameters();
                NumArgs = @params.Length;
                ArgTypes = @params.Select(x => x.ParameterType).ToArray();
                Method = _ConstructorMethods.Get(con);
            }
        }

        /// <summary>
        /// Updates one object with the fields of another.
        /// </summary>
        public static void UpdateFieldsFrom<T>(this T ToObj, T FromObj) {
            UpdateFieldsFrom(ToObj, FromObj, new Dictionary<object, object>());
        }

        private static void UpdateFieldsFrom<T>(T ToObj, T FromObj, Dictionary<object, object> Values) {
            Values.Add(FromObj, ToObj);

            var fields = ToObj.GetType().GetCachedFields();
            object value = null;
            object othervalue = null;
            foreach (var field in fields) {
                value = field.FastGet(FromObj);
                othervalue = field.FastGet(ToObj);
                if (object.Equals(othervalue, value)) continue;

                if (value != null && Values.ContainsKey(value)) {
                    //Prevent recursive calls: objA.fieldB.fieldC = objA

                    field.FastSet(ToObj, Values[value]);
                } else if (othervalue != null && value != null && !field.FieldType.IsPrimitive && !field.FieldType.IsValueType && !(value is string) && field.FieldType.GetCachedFields().Length > 0) {

                    //Do not overwrite object references
                    UpdateFieldsFrom(othervalue, value, Values);
                } else {
                    field.FastSet(ToObj, value);
                }
            }
        }

        public static bool IsMarkedSerializable(this Type type) {
            return _MarkedSerializable.Get(type);
        }

        public static PropertyInfo[] GetCachedProperties(this Type type) {
            return _Properties.Get(type);
        }

        public static FieldInfo[] GetSerializableFields(this Type type) {
            return _SerializableFields.Get(type);
        }

        public static FieldInfo[] GetCachedFields(this Type type) {
            return _Fields.Get(type);
        }

        public static object FastInvoke(this MethodInfo method, object instance, object[] @params) {
            return method.MethodHandle.FastInvoke(instance, @params);
        }

        public static object FastInvoke(this RuntimeMethodHandle handle, object instance, object[] @params) {
            Delegate del = _Invokers.Get(handle);

            if (del is Action<object, object[]>) {
                ((Action<object, object[]>)del)(instance, @params);
                return null;
            } else {
                return ((Func<object, object[], object>)del)(instance, @params);
            }
        }

        public static object FastCreate(this Type type) {
            return FastCreate(type, null);
        }

        public static T FastCreate<T>() {
            return (T)FastCreate(typeof(T), null);
        }

        public static Constructor[] GetCachedConstructors(this Type type) {
            return _Constructors.Get(type);
        }

        public static object FastCreate(this Type type, params object[] @params) {
            //Activator.CreateInstance is stupid fast for value types and stupid slow for class types
            if (type.IsValueType) return Activator.CreateInstance(type, @params);

            var constructors = type.GetCachedConstructors();
            int numargs = @params == null ? 0 : @params.Length;
            var temp = constructors.Where(x => x.NumArgs == numargs);

            Func<object[], object> del = null;
            if (temp.Count() == 1 || numargs == 0) {
                del = temp.First().Method;
            } else {
                for (int i = 0; i < numargs; i++) {
                    if (object.Equals(@params[i], null)) continue;
                    temp = temp.Where(x => x.ArgTypes[i] == @params[i].GetType());
                    int count = temp.Count();
                    if (count == 0) {
                        del = constructors.First().Method;
                        break;
                    }else if (count == 1) {
                        del = temp.First().Method;
                        break;
                    }
                }
            }

            if(del == null) return Activator.CreateInstance(type, @params);
            else return del(@params);
        }

        public static object FastInvoke(this ConstructorInfo method, object[] @params) {
            Delegate del = _ConstructorMethods.Get(method);

            return ((Func<object[], object>)del).Invoke(@params);
        }

        public static void FastSet(this FieldInfo field, object target, object value) {
            var setter = _FieldSetters.Get(field);

            try {
                setter(target, value);
            } catch (InvalidCastException) {
                setter = CreateSetFieldDelegate(field);
                lock (_FieldSetters) 
                    _FieldSetters[field] = setter;
                setter(target, value);
            }
        }

        public static object FastGet(this FieldInfo field, object target) {
            var getter = _FieldGetters.Get(field);
            
            try {
                return getter(target);
            } catch (InvalidCastException) {
                getter = CreateGetFieldDelegate(field);
                _FieldGetters[field] = getter;
                
                return getter(target);
            }
        }

        public static object FastGet(this PropertyInfo prop, object target) {
            var getter =  _PropGetters.Get(prop);
            try {
                return getter(target);
            } catch (InvalidCastException) {
                getter = CreateGetPropertyDelegate(prop);
                    _PropGetters[prop] = getter;

                return getter(target);
            }
        }

        public static void FastSet(this PropertyInfo prop, object target, object value) {
            var setter = _PropSetters.Get(prop);

            try {
                setter(target, new[] { value });
            } catch (InvalidCastException) {
                setter = CreateSetPropertyDelegate(prop);
                _PropSetters[prop] = setter;

                setter(target, new[] { value });
            }
        }


        #region Delegate Creation
        private static Func<object[], object> CreateConstructor(ConstructorInfo constructorInfo) {
            // parameters to execute
            var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

            // build parameter list
            var parameterExpressions = new List<Expression>();
            var paramInfos = constructorInfo.GetParameters();
            for (int i = 0; i <= paramInfos.Length - 1; i++) {
                // (Ti)parameters[i]
                var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfos[i].ParameterType);

                parameterExpressions.Add(valueCast);
            }

            // new T((T0)parameters[0], (T1)parameters[1], ...)
            var instanceCreate = Expression.New(constructorInfo, parameterExpressions);

            // (object)new T((T0)parameters[0], (T1)parameters[1], ...)
            var instanceCreateCast = Expression.Convert(instanceCreate, typeof(object));

            var lambda = Expression.Lambda<Func<object[], object>>(instanceCreateCast, parametersParameter);

            return lambda.Compile();
        }

        private static Action<object, object> CreateSetFieldDelegate(FieldInfo field) {
            DynamicMethod dm = new DynamicMethod(string.Concat(field.DeclaringType.FullName, ".Set_", field.Name), null, new Type[] { typeof(object), typeof(object) }, typeof(object), true);
            ILGenerator il = dm.GetILGenerator();

            // Load the instance of the object (argument 0) onto the stack
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);

            if (field.FieldType.IsValueType) {
                il.Emit(OpCodes.Unbox, field.FieldType);
                il.Emit(OpCodes.Ldobj, field.FieldType);
            } else {
                il.Emit(OpCodes.Castclass, field.FieldType);
            }
            // Load the value of the object's field (fi) onto the stack
            il.Emit(OpCodes.Stfld, field);

            // return the value on the top of the stack
            il.Emit(OpCodes.Ret);

            return (Action<object, object>)dm.CreateDelegate(typeof(Action<object, object>));
        }

        private static Func<object, object> CreateGetPropertyDelegate(PropertyInfo prop) {
            var instance = Expression.Parameter(typeof(object), "instance");
            var instCast = prop.GetGetMethod(true).IsStatic ? null : Expression.Convert(instance, prop.ReflectedType);
            var propAcc = Expression.Property(instCast, prop);
            var castProp = Expression.Convert(propAcc, typeof(object));
            var lambda = Expression.Lambda<Func<object, object>>(castProp, instance);

            return lambda.Compile();
        }

        private static Action<object, object[]> CreateSetPropertyDelegate(PropertyInfo prop) {
            return CreateSubMethodDelegate(prop.GetSetMethod(true));
        }

        private static Action<object, object[]> CreateSubMethodDelegate(MethodInfo meth) {
            var instance = Expression.Parameter(typeof(object), "instance");
            var @params = Expression.Parameter(typeof(object[]), "parameters");
            var paramExp = new List<Expression>();
            var paramInfos = meth.GetParameters();
            for (int i = 0; i <= paramInfos.Length - 1; i++) {
                var valueObj = Expression.ArrayIndex(@params, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfos[i].ParameterType);

                paramExp.Add(valueCast);
            }

            var instCast = meth.IsStatic ? null : Expression.Convert(instance, meth.ReflectedType);
            var methCall = Expression.Call(instCast, meth, paramExp);

            var lambda = Expression.Lambda<Action<object, object[]>>(methCall, instance, @params);
            return lambda.Compile();
        }

        private static Func<object, object[], object> CreateFunctionMethodDelegate(MethodInfo meth) {
            var instance = Expression.Parameter(typeof(object), "instance");
            var @params = Expression.Parameter(typeof(object[]), "parameters");
            var paramExp = new List<Expression>();
            var paramInfos = meth.GetParameters();
            for (int i = 0; i <= paramInfos.Length - 1; i++) {
                var valueObj = Expression.ArrayIndex(@params, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfos[i].ParameterType);

                paramExp.Add(valueCast);
            }

            var instCast = meth.IsStatic ? null : Expression.Convert(instance, meth.ReflectedType);
            var methCall = Expression.Call(instCast, meth, paramExp);

            var castMath = Expression.Convert(methCall, typeof(object));
            var lambda = Expression.Lambda<Func<object, object[], object>>(castMath, instance, @params);
            return lambda.Compile();
        }
  
        private static Func<object, object> CreateGetFieldDelegate(FieldInfo field) {
            var instance = Expression.Parameter(typeof(object), "instance");
            var instCast = field.IsStatic ? null : Expression.Convert(instance, field.ReflectedType);
            var fieldacc = Expression.Field(instCast, field);
            var castProp = Expression.Convert(fieldacc, typeof(object));
            var lambda = Expression.Lambda<Func<object, object>>(castProp, instance);

            return lambda.Compile();
        }
        #endregion
    }
}